﻿//-----------------------------------------------------------------------
// Source File: "Form1.cs"
// Create Date: 07/30/2014 10:16 AM
// Last Updated: 07/30/2014 10:16 AM
// Authors(s): nealbailey@hotmail.com
//-----------------------------------------------------------------------
// GNU GENERAL PUBLIC LICENSE
//-----------------------------------------------------------------------
// Revision: Version 3, 29 June 2007
// Copyright © 2007 Free Software Foundation, Inc. http://fsf.org/
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//  GNU General Public License for more details.
//  REFERENCE: http://www.gnu.org/licenses/
//-----------------------------------------------------------------------
// Copyright (c) 2010-2014 Baileysoft Solutions
//-----------------------------------------------------------------------

namespace Baileysoft.WinForms
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Diagnostics;
    using System.Drawing;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Threading;
    using System.Windows.Forms;

    using Baileysoft.Gaming.Utils.Yasm;
    using Baileysoft.Monitor.Core.Utilities;

    using BrightIdeasSoftware;

    using HP.EnterpriseServices.Settings;

    #region Delegates

    /// <summary>
    /// <see cref="EventArgs"/> delegate for raising Form1.ListViewRefreshed event.
    /// </summary>
    /// <param name="sender">The <see cref="object"/> that triggered the event.</param>
    /// <param name="e">The <see cref="FileCloneErrorEventArgs"/> related to the event.</param>
    public delegate void RefreshEventHandler(object sender, RefreshEventArgs e);

    #endregion Delegates

    /// <summary>
    /// Main <see cref="Form"/> of the application.
    /// </summary>
    public partial class Form1 : Form
    {
        #region Fields

        /// <summary>
        /// Global serialized <see cref="Settings"/>
        /// </summary>
        protected internal Settings Settings;

        /// <summary>
        /// Local reference to <see cref="DirectoryCloner"/>
        /// </summary>
        private DirectoryCloner _directoryCloner = null;

        /// <summary>
        /// A <see cref="System.Diagnostics.Stopwatch"/> for tracking intervals
        /// </summary>
        private Stopwatch _stopwatch = null;

        #endregion Fields

        #region Constructors

        /// <summary>
        /// Constructor.
        /// </summary>
        public Form1()
        {
            InitializeComponent();
            this.ListViewRefreshed += OnListViewRefreshed;
        }

        #endregion Constructors

        #region Events

        /// <summary>
        /// Raise the ListViewRefrehsed event
        /// </summary>
        public event RefreshEventHandler ListViewRefreshed;

        #endregion Events

        #region Properties

        /// <summary>
        /// Indicates that the process of hashing files is in progress.
        /// </summary>
        protected internal virtual bool DirectoryCloningInProgress { get; set; }

        /// <summary>
        /// Indicates that the refresh source process is in progress.
        /// </summary>
        protected internal virtual bool RefreshThreadRunning { get; set; }

        #endregion Properties

        #region Methods

        /// <summary>
        /// Refresh the <see cref="ObjectListView"/> with accurate info (asynchronous)
        /// </summary>
        protected internal virtual void BeginRefreshListView()
        {
            RefreshThreadRunning = true;
            tspBtnRefresh.Enabled = !RefreshThreadRunning;
            tspBtnCancel.Enabled = RefreshThreadRunning;
            btnBrowseSource.Enabled = !RefreshThreadRunning;
            btnBrowseTarget.Enabled = !RefreshThreadRunning;

            SetSuccessStatus("Started fetching source folder directory entries.");

            var args = new RefreshListViewArgs(cbSourceFolder.Text, txtTargetFolder.Text);
            var thread = new Thread(() => RefreshListView(args));
            thread.Start();
        }

        /// <summary>
        /// Handler for browse for folder (Browse...) button clicks
        /// </summary>
        [DebuggerNonUserCode]
        protected internal virtual void BrowseForDestinationFolder()
        {
            FolderBrowserDialog folderBrowser;
            using (folderBrowser = new FolderBrowserDialog())
            {
                folderBrowser.Description = "Select the destination (HDD) folder.";
                folderBrowser.ShowNewFolderButton = false;
                if (!string.IsNullOrEmpty(txtTargetFolder.Text))
                    folderBrowser.SelectedPath = txtTargetFolder.Text;

                if (folderBrowser.ShowDialog(this) == DialogResult.OK)
                    if (!string.IsNullOrEmpty(folderBrowser.SelectedPath))
                        txtTargetFolder.Text = folderBrowser.SelectedPath;
            }
        }

        /// <summary>
        /// Handler for browse for folder (Browse...) button clicks
        /// </summary>
        [DebuggerNonUserCode]
        protected internal virtual void BrowseForSourceFolder()
        {
            FolderBrowserDialog folderBrowser;
            using (folderBrowser = new FolderBrowserDialog())
            {
                folderBrowser.Description = "Select the source (SSD) folder.";
                folderBrowser.ShowNewFolderButton = false;
                if (!string.IsNullOrEmpty(cbSourceFolder.Text))
                    folderBrowser.SelectedPath = cbSourceFolder.Text;

                if (folderBrowser.ShowDialog(this) == DialogResult.OK)
                    if (!string.IsNullOrEmpty(folderBrowser.SelectedPath))
                        cbSourceFolder.Text = folderBrowser.SelectedPath;
            }
        }

        /// <summary>
        /// Tasks to execute when an error occurs during the directory move process.
        /// </summary>
        /// <param name="sender">The <see cref="object"/> that triggered the event.</param>
        /// <param name="e">The <see cref="FileCloneErrorEventArgs"/> related to the event.</param>
        /// <exception cref="System.NullReferenceException">The exception that is thrown when there is an attempt to dereference a null object reference.</exception>
        protected internal virtual void FileCloneErrorOccurred(object sender, FileCloneErrorEventArgs e)
        {
            // Use anonymous delegate to pass control from background thread to UI thread.
            if (lbLogWindow.InvokeRequired)
                lbLogWindow.Invoke(new MethodInvoker(delegate { FileCloneErrorOccurred(sender, e); }));
            else
            {
                SetErrorStatus(e.Exception.Message);
                LogMessageToWindow("Error - " + e.Exception.Message);
            }
        }

        /// <summary>
        /// Tasks to execute when the directory move process completes.
        /// </summary>
        /// <param name="sender">The <see cref="object"/> that triggered the event.</param>
        /// <param name="e">The <see cref="DirectoryCloneEventArgs"/> related to the event.</param>
        /// <exception cref="System.ArgumentOutOfRangeException">The exception that is thrown when the value of an argument is outside the allowable range of values as defined by the invoked method.</exception>
        /// <exception cref="System.NullReferenceException">The exception that is thrown when there is an attempt to dereference a null object reference.</exception>
        protected internal virtual void FileCloneTaskCompleted(object sender, DirectoryCloneEventArgs e)
        {
            // Use anonymous delegate to pass control from background thread to UI thread.
            if (lbLogWindow.InvokeRequired)
                lbLogWindow.Invoke(new MethodInvoker(delegate { FileCloneTaskCompleted(sender, e); }));
            else
            {
                if (_stopwatch != null)
                {
                    _stopwatch.Stop();
                    LogMessageToWindow(string.Format(
                        "Thread exited successfully. The thread executed for a duration of: '{0}'.", 
                            _stopwatch.Elapsed));
                }

                SetSuccessStatus(e.Message);
                tabControl1.SelectedIndex = 0;

                DirectoryCloningInProgress = false;
                tspBtnRefresh.Enabled = !DirectoryCloningInProgress;
                tspBtnCancel.Enabled = DirectoryCloningInProgress;
                btnBrowseSource.Enabled = !DirectoryCloningInProgress;
                btnBrowseTarget.Enabled = !DirectoryCloningInProgress;

                tspBtnRefresh.PerformClick();
            }
        }

        /// <summary>
        /// Tasks to execute when a directory move process task has begun.
        /// </summary>
        /// <param name="sender">The <see cref="object"/> that triggered the event.</param>
        /// <param name="e">The <see cref="DirectoryCloneEventArgs"/> related to the event.</param>
        /// <exception cref="System.NullReferenceException">The exception that is thrown when there is an attempt to dereference a null object reference.</exception>
        protected internal virtual void FileCloneTaskStarted(object sender, DirectoryCloneEventArgs e)
        {
            // Use anonymous delegate to pass control from background thread to UI thread.
            if (lbLogWindow.InvokeRequired)
                lbLogWindow.Invoke(new MethodInvoker(delegate { FileCloneTaskStarted(sender, e); }));
            else
            {
                LogMessageToWindow(e.Message);
                SetSuccessStatus(e.Message);
                //lbLogWindow.SelectedIndex = lbLogWindow.Items.Count - 1;
            }
        }

        /// <summary>
        /// Update the log <see cref="ListBox"/> window with details the user might want to know.
        /// </summary>
        /// <param name="message"><see cref="string"/></param>
        /// <exception cref="System.FormatException">The exception that is thrown when the format of an argument does not meet the parameter specifications of the invoked method.</exception>
        /// <exception cref="System.ArgumentNullException">The exception that is thrown when a null reference (Nothing in Visual Basic) is passed to a method that does not accept it as a valid argument.</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">The exception that is thrown when the value of an argument is outside the allowable range of values as defined by the invoked method.</exception>
        /// <exception cref="System.SystemException">Defines the base class for predefined exceptions in the System namespace.</exception>
        protected internal virtual void LogMessageToWindow(string message)
        {
            if (string.IsNullOrEmpty(message))
                return;

            var datePrefix = DateTime.Now.ToString("yyyy-MM-dd hh:mm tt");
            lbLogWindow.Items.Add(string.Format("{0} | {1}", datePrefix, message));
        }

        /// <summary>
        /// User clicked the browse for destination folder button
        /// </summary>
        /// <param name="sender">The <see cref="object"/> that triggered the event.</param>
        /// <param name="e">The <see cref="EventArgs"/> related to the event.</param>
        protected internal virtual void OnBrowseForDestFolder(object sender, EventArgs e)
        {
            BrowseForDestinationFolder();
        }

        /// <summary>
        /// User clicked the browse for source folder button
        /// </summary>
        /// <param name="sender">The <see cref="object"/> that triggered the event.</param>
        /// <param name="e">The <see cref="EventArgs"/> related to the event.</param>
        protected internal virtual void OnBrowseForSourceFolder(object sender, EventArgs e)
        {
            BrowseForSourceFolder();
        }

        /// <summary>
        /// User clicked the cancel button
        /// </summary>
        /// <param name="sender">The <see cref="object"/> that triggered the event.</param>
        /// <param name="e">The <see cref="EventArgs"/> related to the event.</param>
        protected internal virtual void OnCancelClick(object sender, EventArgs e)
        {
            if (_directoryCloner != null)
                _directoryCloner.IsTaskCancelled = true;
        }

        /// <summary>
        /// User clicked the clear log window menu item
        /// </summary>
        /// <param name="sender">The <see cref="object"/> that triggered the event.</param>
        /// <param name="e">The <see cref="EventArgs"/> related to the event.</param>
        protected internal virtual void OnClearLogWindowClick(object sender, EventArgs e)
        {
            lbLogWindow.Items.Clear();
        }

        /// <summary>
        /// Event handler for whenever the form is closing.
        /// </summary>
        /// <param name="sender">The <see cref="object"/> that triggered the event.</param>
        /// <param name="e">The <see cref="EventArgs"/> related to the event.</param>
        /// <exception cref="System.ArgumentNullException">The exception that is thrown when a null reference (Nothing in Visual Basic) is passed to a method that does not accept it as a valid argument.</exception>
        /// <exception cref="System.ComponentModel.InvalidEnumArgumentException">The exception thrown when using invalid arguments that are enumerators.</exception>
        /// <exception cref="System.FormatException">The exception that is thrown when the format of an argument does not meet the parameter specifications of the invoked method.</exception>
        protected internal virtual void OnFormClosing(object sender, FormClosingEventArgs e)
        {
            //ToDo: Block form close if move thread is running
            // e.Cancel = true;

            try
            {
                // Persist window positions
                Settings.WindowPosition =
                    String.Format("{0},{1},{2},{3}",
                                  (double) Width,
                                  (double) Height,
                                  (double) Location.X,
                                  (double) Location.Y);

                // Persist window state
                Settings.WindowState = Convert.ToString(
                    this.WindowState, System.Globalization.CultureInfo.CurrentCulture);

                Settings.Save();

                this.ListViewRefreshed -= OnListViewRefreshed;

                if (_directoryCloner != null)
                {
                    _directoryCloner.FileCloneTaskStarted -= FileCloneTaskStarted;
                    _directoryCloner.FileCloneTaskCompleted -= FileCloneTaskCompleted;
                    _directoryCloner.FileCloneErrorOccurred -= FileCloneErrorOccurred;
                }
            }
            catch //Urgent: Empty catch
            {
            }
        }

        /// <summary>
        /// Event handler for whenever the form is loaded.
        /// </summary>
        /// <param name="sender">The <see cref="object"/> that triggered the event.</param>
        /// <param name="e">The <see cref="EventArgs"/> related to the event.</param>
        /// <exception cref="System.ArgumentNullException">The exception that is thrown when a null reference (Nothing in Visual Basic) is passed to a method that does not accept it as a valid argument.</exception>
        /// <exception cref="System.ArgumentException">The exception that is thrown when one of the arguments provided to a method is not valid.</exception>
        /// <exception cref="System.FormatException">The exception that is thrown when the format of an argument does not meet the parameter specifications of the invoked method.</exception>
        /// <exception cref="System.OverflowException">The exception that is thrown when an arithmetic, casting, or conversion operation in a checked context results in an overflow.</exception>
        /// <exception cref="System.NullReferenceException">The exception that is thrown when there is an attempt to dereference a null object reference.</exception>
        protected internal virtual void OnFormLoad(object sender, EventArgs e)
        {
            SetupForm();
            WriteLoggerPreamble();
            SetSuccessStatus("Application completed loading process successfully.");
        }

        /// <summary>
        /// Operations that should occur once the data needed to populate the <see cref="ObjectListView"/>.
        /// </summary>
        /// <param name="sender">The <see cref="object"/> that triggered the event.</param>
        /// <param name="e">The <see cref="RefreshEventArgs"/> related to the event.</param>
        /// <exception cref="System.ArgumentNullException">The exception that is thrown when a null reference (Nothing in Visual Basic) is passed to a method that does not accept it as a valid argument.</exception>
        /// <exception cref="System.IndexOutOfRangeException">The exception that is thrown when an attempt is made to access an element of an array with an index that is outside the bounds of the array. This class cannot be inherited.</exception>
        /// <exception cref="System.FormatException">The exception that is thrown when the format of an argument does not meet the parameter specifications of the invoked method.</exception>
        /// <exception cref="System.NullReferenceException">The exception that is thrown when there is an attempt to dereference a null object reference.</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">The exception that is thrown when the value of an argument is outside the allowable range of values as defined by the invoked method.</exception>
        /// <exception cref="System.SystemException">Defines the base class for predefined exceptions in the System namespace.</exception>
        protected internal virtual void OnListViewRefreshed(object sender, RefreshEventArgs e)
        {
            // Use anonymous delegate to pass control from background thread to UI thread.
            if (this.InvokeRequired)
                this.Invoke(new MethodInvoker(delegate { OnListViewRefreshed(sender, e); }));
            else
            {
                RefreshThreadRunning = false;
                tspBtnRefresh.Enabled = !RefreshThreadRunning;
                tspBtnCancel.Enabled = RefreshThreadRunning;
                btnBrowseSource.Enabled = !RefreshThreadRunning;
                btnBrowseTarget.Enabled = !RefreshThreadRunning;

                if (e.FolderPairs.Count > 0)
                {
                    LogMessageToWindow(string.Format("Source path has {0} of available free space.",
                                                     e.FolderPairs[0].GetSourceDriveFreeSpace()));
                    LogMessageToWindow(string.Format("Destination path has {0} of available free space.",
                                                     e.FolderPairs[0].GetDestinationDriveFreeSpace()));
                }

                // Define a delegate to insert an image from an image-list to display in a sub-item
                junctionDirectionColumn.ImageGetter =
                    delegate(object fObject)
                        {
                            var folderPair = (FolderPair) fObject;
                            return string.IsNullOrEmpty(folderPair.SourceFolder.JunctionPointTarget) ? "left" : "right";
                        };

                // Limit the image cell width to 18 pixels
                junctionDirectionColumn.Width = 18;

                // Bind the ObjectListView
                lvFolders.SetObjects(e.FolderPairs);

                // Bind the ObjectListView
                lvFolders.SetObjects(e.FolderPairs);

                SetSuccessStatus(e.Message);
                LogMessageToWindow(e.Message);
            }
        }

        /// <summary>
        /// User selected an item in the lvFolders <see cref="ObjectListView"/>.
        /// </summary>
        /// <param name="sender">The <see cref="object"/> that triggered the event.</param>
        /// <param name="e">The <see cref="EventArgs"/> related to the event.</param>
        protected internal virtual void OnLvFoldersSelectedIndexChanged(object sender, EventArgs e)
        {
            var objectListView = sender as ObjectListView;
            if (objectListView != null)
            {
                var listView = objectListView;
                if (listView.SelectedItem == null)
                    return;

                var folderPair = (FolderPair)listView.SelectedItem.RowObject;
                tspBtnJunctionLeft.Enabled = (!string.IsNullOrEmpty(folderPair.SourceFolder.JunctionPointTarget));
                tspBtnJunctionRight.Enabled = (string.IsNullOrEmpty(folderPair.SourceFolder.JunctionPointTarget));
            }
        }

        /// <summary>
        /// User clicked the open log file menu item
        /// </summary>
        /// <param name="sender">The <see cref="object"/> that triggered the event.</param>
        /// <param name="e">The <see cref="EventArgs"/> related to the event.</param>
        /// <exception cref="System.ArgumentException">The exception that is thrown when one of the arguments provided to a method is not valid.</exception>
        /// <exception cref="System.ArgumentNullException">The exception that is thrown when a null reference (Nothing in Visual Basic) is passed to a method that does not accept it as a valid argument.</exception>
        /// <exception cref="System.IO.DirectoryNotFoundException">The exception that is thrown when part of a file or directory cannot be found.</exception>
        /// <exception cref="System.IO.PathTooLongException">The exception that is thrown when a pathname or filename is longer than the system-defined maximum length.</exception>
        /// <exception cref="System.Security.SecurityException">The exception that is thrown when a security error is detected.</exception>
        /// <exception cref="System.UnauthorizedAccessException">The exception that is thrown when the operating system denies access because of an I/O error or a specific type of security error.</exception>
        /// <exception cref="System.ObjectDisposedException">The exception that is thrown when an operation is performed on a disposed object.</exception>
        /// <exception cref="System.IO.IOException">The exception that is thrown when an I/O error occurs.</exception>
        /// <exception cref="System.ComponentModel.Win32Exception">Throws an exception for a Win32 error code.</exception>
        /// <exception cref="System.IO.FileNotFoundException">The exception that is thrown when an attempt to access a file that does not exist on disk fails.</exception>
        protected internal virtual void OnOpenLogFileClick(object sender, EventArgs e)
        {
            var tmpLog = Path.GetTempFileName() + ".log";
            using (var sw = new StreamWriter(tmpLog))
                foreach (var line in lbLogWindow.Items)
                    sw.WriteLine(line);

            Process.Start(tmpLog);
        }

        /// <summary>
        /// User clicked the refresh source folder button
        /// </summary>
        /// <param name="sender">The <see cref="object"/> that triggered the event.</param>
        /// <param name="e">The <see cref="EventArgs"/> related to the event.</param>
        /// <exception cref="System.NullReferenceException">The exception that is thrown when there is an attempt to dereference a null object reference.</exception>
        /// <exception cref="System.ArgumentException">The exception that is thrown when one of the arguments provided to a method is not valid.</exception>
        /// <exception cref="System.ArgumentNullException">The exception that is thrown when a null reference (Nothing in Visual Basic) is passed to a method that does not accept it as a valid argument.</exception>
        /// <exception cref="System.UnauthorizedAccessException">The exception that is thrown when the operating system denies access because of an I/O error or a specific type of security error.</exception>
        /// <exception cref="System.IO.PathTooLongException">The exception that is thrown when a pathname or filename is longer than the system-defined maximum length.</exception>
        /// <exception cref="System.IO.IOException">The exception that is thrown when an I/O error occurs.</exception>
        /// <exception cref="System.IO.DirectoryNotFoundException">The exception that is thrown when part of a file or directory cannot be found.</exception>
        /// <exception cref="System.FormatException">The exception that is thrown when the format of an argument does not meet the parameter specifications of the invoked method.</exception>
        protected internal virtual void OnRefresh(object sender, EventArgs e)
        {
            if (ParametersAreValid())
            {
                PersistFolderPairs();
                BeginRefreshListView();
            }
        }

        /// <summary>
        /// Event handler for whenever the Toolbar's Settings button is clicked.
        /// </summary>
        /// <remarks>Sender and event arguments are unused and may be null for unit testing.</remarks>
        /// <param name="sender">The <see cref="object"/> that triggered the event.</param>
        /// <param name="e">The <see cref="EventArgs"/> related to the event.</param>
        /// <exception cref="System.InvalidOperationException">The exception that is thrown when a method call is invalid for the object's current state.</exception>
        protected internal virtual void OnSettingsClick(object sender, EventArgs e)
        {
            var formSettings = new FrmSettings(Settings);
            try
            {
                formSettings.ShowDialog();
            }
            catch (Exception ex)
            {
                //LogError("Unable to View/Change Settings: " + ex.Message);
                LogMessageToWindow("Unable to View/Change Settings: " + ex.Message);
            }
            finally
            {
                formSettings.Dispose();
            }
        }

        /// <summary>
        /// User selected an item from the cbSourceFolder <see cref="ComboBox"/>
        /// </summary>
        /// <param name="sender">The <see cref="object"/> that triggered the event.</param>
        /// <param name="e">The <see cref="EventArgs"/> related to the event.</param>
        /// <exception cref="System.NullReferenceException">The exception that is thrown when there is an attempt to dereference a null object reference.</exception>
        /// <exception cref="System.ArgumentException">The exception that is thrown when one of the arguments provided to a method is not valid.</exception>
        /// <exception cref="System.ArgumentNullException">The exception that is thrown when a null reference (Nothing in Visual Basic) is passed to a method that does not accept it as a valid argument.</exception>
        /// <exception cref="System.UnauthorizedAccessException">The exception that is thrown when the operating system denies access because of an I/O error or a specific type of security error.</exception>
        /// <exception cref="System.IO.PathTooLongException">The exception that is thrown when a pathname or filename is longer than the system-defined maximum length.</exception>
        /// <exception cref="System.IO.IOException">The exception that is thrown when an I/O error occurs.</exception>
        /// <exception cref="System.IO.DirectoryNotFoundException">The exception that is thrown when part of a file or directory cannot be found.</exception>
        /// <exception cref="System.FormatException">The exception that is thrown when the format of an argument does not meet the parameter specifications of the invoked method.</exception>
        protected internal virtual void OnSourceFolderSelectedIndexChanged(object sender, EventArgs e)
        {
            // Convert delimited folder pairs into a dictionary, fetch the value, insert into txtTargetFolder text-box
            txtTargetFolder.Text =
                Settings.ParseYasmFolderPairsToDict().ToList().SingleOrDefault(
                    p => p.Key.Equals(cbSourceFolder.Text)).Value;

            lvFolders.Items.Clear();
            OnRefresh(this, EventArgs.Empty);
        }

        /// <summary>
        /// User clicked the Junction Left button
        /// </summary>
        /// <param name="sender">The <see cref="object"/> that triggered the event.</param>
        /// <param name="e">The <see cref="EventArgs"/> related to the event.</param>
        /// <exception cref="System.NullReferenceException">The exception that is thrown when there is an attempt to dereference a null object reference.</exception>
        /// <exception cref="System.ArgumentException">The exception that is thrown when one of the arguments provided to a method is not valid.</exception>
        /// <exception cref="System.ArgumentNullException">The exception that is thrown when a null reference (Nothing in Visual Basic) is passed to a method that does not accept it as a valid argument.</exception>
        protected internal virtual void OnTspBtnJunctionLeftClick(object sender, EventArgs e)
        {
            if (lvFolders.SelectedItems.Count < 1)
                return;

            if (!ParametersAreValid())
                return;

            PersistFolderPairs();

            tabControl1.SelectedIndex = 1;
            tspBtnJunctionLeft.Enabled = false;

            var selectedItem = (FolderPair) lvFolders.SelectedItem.RowObject;

            // Did user update destination?
            if (selectedItem.DestinationFolder != null &&
                selectedItem.DestinationFolder != txtTargetFolder.Text)
            {
                selectedItem.DestinationFolder =
                    Path.Combine(txtTargetFolder.Text,
                        selectedItem.SourceFolder.FolderName);
            }

            StartCloning(selectedItem);
        }

        /// <summary>
        /// User clicked the Junction Right button
        /// </summary>
        /// <param name="sender">The <see cref="object"/> that triggered the event.</param>
        /// <param name="e">The <see cref="EventArgs"/> related to the event.</param>
        /// <exception cref="System.NullReferenceException">The exception that is thrown when there is an attempt to dereference a null object reference.</exception>
        /// <exception cref="System.ArgumentException">The exception that is thrown when one of the arguments provided to a method is not valid.</exception>
        /// <exception cref="System.ArgumentNullException">The exception that is thrown when a null reference (Nothing in Visual Basic) is passed to a method that does not accept it as a valid argument.</exception>
        protected internal virtual void OnTspBtnJunctionRightClick(object sender, EventArgs e)
        {
            if (lvFolders.SelectedItems.Count < 1)
                return;

            if (!ParametersAreValid())
                return;

            PersistFolderPairs();

            tabControl1.SelectedIndex = 1;
            tspBtnJunctionRight.Enabled = false;

            var selectedItem = (FolderPair) lvFolders.SelectedItem.RowObject;

            // Did user update destination?
            if (selectedItem.DestinationFolder != null &&
                selectedItem.DestinationFolder != txtTargetFolder.Text)
            {
                selectedItem.DestinationFolder =
                    Path.Combine(txtTargetFolder.Text,
                                 selectedItem.SourceFolder.FolderName);
            }

            StartCloning(selectedItem);
        }

        /// <summary>
        /// Validate rules around source and target directory values.
        /// </summary>
        /// <returns>A <see cref="bool"/> flag; true if valid false otherwise.</returns>
        protected internal virtual bool ParametersAreValid()
        {
            if (string.IsNullOrEmpty(cbSourceFolder.Text))
            {
                SetErrorStatus("You must enter a valid source folder path.");
                return false;
            }
            if (string.IsNullOrEmpty(txtTargetFolder.Text))
            {
                SetErrorStatus("You must enter a valid destination folder path.");
                return false;
            }

            if (cbSourceFolder.Text.Equals(txtTargetFolder.Text))
            {
                SetErrorStatus("Source and target paths cannot be the same.");
                return false;
            }

            if (!Directory.Exists(cbSourceFolder.Text))
            {
                SetErrorStatus("Source folder does not exist: " + cbSourceFolder.Text);
                return false;
            }

            if (!Directory.Exists(txtTargetFolder.Text))
            {
                SetErrorStatus("Destination folder does not exist: " + txtTargetFolder.Text);
                return false;
            }

            return true;
        }

        /// <summary>
        /// Update the application UI and settings to reflect user-defined folder pair
        /// </summary>
        /// <exception cref="System.NullReferenceException">The exception that is thrown when there is an attempt to dereference a null object reference.</exception>
        /// <exception cref="System.ArgumentException">The exception that is thrown when one of the arguments provided to a method is not valid.</exception>
        /// <exception cref="System.ArgumentNullException">The exception that is thrown when a null reference (Nothing in Visual Basic) is passed to a method that does not accept it as a valid argument.</exception>
        protected internal virtual void PersistFolderPairs()
        {
            var pairDict = Settings.ParseYasmFolderPairsToDict();
            if (pairDict.ContainsKey(cbSourceFolder.Text)) //update existing
            {
                if (pairDict.ContainsKey(cbSourceFolder.Text) &&
                    pairDict[cbSourceFolder.Text] != txtTargetFolder.Text)
                {
                    pairDict[cbSourceFolder.Text] = txtTargetFolder.Text;
                }
            }
            else
            {
                pairDict.Add(cbSourceFolder.Text, txtTargetFolder.Text);
            }

            // Ensure the user-entered folder pair gets into serialized Settings
            Settings.SaveFolderPairs(pairDict);

            // Add new source folder into combo-box
            if (!cbSourceFolder.Items.Contains(cbSourceFolder.Text))
                cbSourceFolder.Items.Add(cbSourceFolder.Text);
        }

        /// <summary>
        /// Raise the <see cref="ListViewRefreshed"/> event
        /// </summary>
        /// <param name="e">The <see cref="RefreshEventArgs"/> related to the event.</param>
        protected internal virtual void RaiseListViewRefreshed(RefreshEventArgs e)
        {
            var handler = ListViewRefreshed;
            if (handler != null)
                handler(this, e);
        }

        /// <summary>
        /// Refresh the <see cref="ObjectListView"/> with accurate info.
        /// </summary>
        /// <param name="args"></param>
        /// <exception cref="System.NullReferenceException">The exception that is thrown when there is an attempt to dereference a null object reference.</exception>
        /// <exception cref="System.UnauthorizedAccessException">The exception that is thrown when the operating system denies access because of an I/O error or a specific type of security error.</exception>
        /// <exception cref="System.ArgumentException">The exception that is thrown when one of the arguments provided to a method is not valid.</exception>
        /// <exception cref="System.ArgumentNullException">The exception that is thrown when a null reference (Nothing in Visual Basic) is passed to a method that does not accept it as a valid argument.</exception>
        /// <exception cref="System.IO.PathTooLongException">The exception that is thrown when a pathname or filename is longer than the system-defined maximum length.</exception>
        /// <exception cref="System.IO.IOException">The exception that is thrown when an I/O error occurs.</exception>
        /// <exception cref="System.IO.DirectoryNotFoundException">The exception that is thrown when part of a file or directory cannot be found.</exception>
        /// <exception cref="System.FormatException">The exception that is thrown when the format of an argument does not meet the parameter specifications of the invoked method.</exception>
        protected internal virtual void RefreshListView(RefreshListViewArgs args)
        {
            var folderPairs = new List<FolderPair>();
            foreach (var folder in Directory.GetDirectories(args.Source))
            {
                var pair = new FolderPair(folder, args.Destination);
                pair.SourceFolder.FormattedSize = pair.SourceFolder.GetReadableFileSize();

                if (JunctionPoint.Exists(pair.SourceFolder.Path))
                    pair.SourceFolder.JunctionPointTarget = JunctionPoint.GetTarget(pair.SourceFolder.Path);

                folderPairs.Add(pair);
            }

            var eventArgs = new RefreshEventArgs(folderPairs,
                string.Format("Refreshed source directory. Found {0} folders on source device '{1}'.",
                    folderPairs.Count, args.Source));
            RaiseListViewRefreshed(eventArgs);
        }

        /// <summary>
        /// Restore the previous window position
        /// </summary>
        /// <exception cref="System.ArgumentNullException">The exception that is thrown when a null reference (Nothing in Visual Basic) is passed to a method that does not accept it as a valid argument.</exception>
        /// <exception cref="System.ArgumentException">The exception that is thrown when one of the arguments provided to a method is not valid.</exception>
        /// <exception cref="System.FormatException">The exception that is thrown when the format of an argument does not meet the parameter specifications of the invoked method.</exception>
        /// <exception cref="System.NullReferenceException">The exception that is thrown when there is an attempt to dereference a null object reference.</exception>
        /// <exception cref="System.OverflowException">The exception that is thrown when an arithmetic, casting, or conversion operation in a checked context results in an overflow.</exception>
        protected internal virtual void RestoreWindow()
        {
            // Valid windows position are: [width],[height],[X],[Y]
            if (!System.Text.RegularExpressions.Regex.IsMatch(
                Settings.WindowPosition, "[0-9,]+"))
            {
                return;
            }

            string[] lastPosition = Settings.WindowPosition.Split(',');
            if (lastPosition.Length >= 3)
            {
                if (Settings.WindowState == "Maximized")
                    this.WindowState = FormWindowState.Maximized;
                else
                {
                    this.Width = Int32.Parse(lastPosition[0]);
                    this.Height = Int32.Parse(lastPosition[1]);
                    Point point = new Point(
                        Int32.Parse(lastPosition[2]),
                            Int32.Parse(lastPosition[3]));

                    this.Location = point;
                    this.StartPosition = FormStartPosition.Manual;
                }
            }
        }

        /// <summary>
        /// Report a failed activity in the status bar.
        /// </summary>
        /// <remarks>
        /// Status bar text is truncated if the string exceeds the pre-defined limit.
        /// </remarks>
        /// <param name="message">Text message to display.</param>
        /// <exception cref="System.NullReferenceException">The exception that is thrown when there is an attempt to dereference a null object reference.</exception>
        protected internal virtual void SetErrorStatus(string message)
        {
            SetErrorStatus(message, false);
        }

        /// <summary>
        /// Report a failed activity in the status bar and optionally display a message box.
        /// </summary>
        /// <remarks>
        /// Status bar text is truncated if the string exceeds the pre-defined limit.
        /// <para>Message box displays all specified text.</para>
        /// </remarks>
        /// <param name="message">Text message to display.</param>
        /// <param name="showMsgBox">Indicates whether a message box is also to be displayed.</param>
        /// <exception cref="System.NullReferenceException">The exception that is thrown when there is an attempt to dereference a null object reference.</exception>
        protected internal virtual void SetErrorStatus(string message, bool showMsgBox)
        {
            //LogMessageToWindow(message);

            tspStatusLabel.Image = Properties.Resources.error;
            if (message == null)
                message = string.Empty;
            if (!message.StartsWith(" "))
                message = message.Insert(0, " ");
            m_lastStatusText = message;
            tspStatusLabel.Text = message;
            TruncateStatusTextToFit();

            if (showMsgBox)
                ShowErrorStatusMsgBox(message, Settings.AppTitle);
        }

        /// <summary>
        /// Report a successful activity in the status bar.
        /// </summary>
        /// <remarks>Text is truncated if the string exceeds the pre-defined limit.</remarks>
        /// <param name="message">text message to display.</param>
        /// <exception cref="System.NullReferenceException">The exception that is thrown when there is an attempt to dereference a null object reference.</exception>
        /// <exception cref="System.ArgumentNullException">The exception that is thrown when a null reference (Nothing in Visual Basic) is passed to a method that does not accept it as a valid argument.</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">The exception that is thrown when the value of an argument is outside the allowable range of values as defined by the invoked method.</exception>
        protected internal virtual void SetSuccessStatus(string message)
        {
            //LogMessageToWindow(message);

            tspStatusLabel.Image = Properties.Resources.info;
            if (message == null)
                message = string.Empty;
            if (!message.StartsWith(" "))
                message = message.Insert(0, " ");
            m_lastStatusText = message;
            tspStatusLabel.Text = message;
            TruncateStatusTextToFit();
        }

        /// <summary>
        /// Set form back to it's last known previous state.
        /// </summary>
        /// <exception cref="System.ArgumentNullException">The exception that is thrown when a null reference (Nothing in Visual Basic) is passed to a method that does not accept it as a valid argument.</exception>
        /// <exception cref="System.ArgumentException">The exception that is thrown when one of the arguments provided to a method is not valid.</exception>
        /// <exception cref="System.FormatException">The exception that is thrown when the format of an argument does not meet the parameter specifications of the invoked method.</exception>
        /// <exception cref="System.OverflowException">The exception that is thrown when an arithmetic, casting, or conversion operation in a checked context results in an overflow.</exception>
        protected internal void SetupForm()
        {
            if (Settings == null)
                // allows a unit test to set the Settings object (including a mocked version) prior to testing this method.
            {
                Settings = new Settings();
            }
            Settings.Load();

            if (string.IsNullOrEmpty(Settings.AppTitle))
                Settings.AppTitle = this.Text; // set by designer

            // Form fonts
            Text = Settings.AppTitle;
            ForeColor = Color.FromName(Settings.UiFontColor);

            Font = new Font(
                Settings.UiFontFace,
                    Int32.Parse(Settings.UiFontSize),
                        FontStyle.Regular);

            tspStatusLabel.Font = new Font(
                Settings.UiFontFace,
                    (Int32.Parse(Settings.UiFontSize) - 1),
                        FontStyle.Regular);

            tabControl1.Font = new Font(
                Settings.YasmListViewFontFace,
                    Int32.Parse(Settings.YasmListViewFontSize),
                        FontStyle.Regular);

            lvFolders.Items.Clear();
            lbLogWindow.Items.Clear();

            // Convert delimited folder pairs into a dictionary and add each key into the source folder combo-box
            Settings.ParseYasmFolderPairsToDict().Keys.ToList().ForEach(p => cbSourceFolder.Items.Add(p));

            RestoreWindow();
        }

        /// <summary>
        /// Displays an error status message box.
        /// </summary>
        /// <param name="message">Text to display.</param>
        /// <param name="title">Message box title text</param>
        /// <returns><see cref="DialogResult"/></returns>
        [DebuggerNonUserCode]
        protected internal virtual DialogResult ShowErrorStatusMsgBox(string message, string title)
        {
            return MessageBox.Show(
                ((message == null) ? string.Empty : message),
                ((title == null) ? string.Empty : title),
                MessageBoxButtons.OK,
                MessageBoxIcon.Error);
        }

        /// <summary>
        /// Begin the directory move process.
        /// </summary>
        /// <param name="parameters"><see cref="FolderPair"/></param>
        /// <exception cref="System.IO.IOException">The exception that is thrown when an I/O error occurs.</exception>
        /// <exception cref="System.ArgumentException">The exception that is thrown when one of the arguments provided to a method is not valid.</exception>
        /// <exception cref="System.ArgumentNullException">The exception that is thrown when a null reference (Nothing in Visual Basic) is passed to a method that does not accept it as a valid argument.</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">The exception that is thrown when the value of an argument is outside the allowable range of values as defined by the invoked method.</exception>
        /// <exception cref="System.UnauthorizedAccessException">The exception that is thrown when the operating system denies access because of an I/O error or a specific type of security error.</exception>
        /// <exception cref="System.IO.PathTooLongException">The exception that is thrown when a pathname or filename is longer than the system-defined maximum length.</exception>
        /// <exception cref="System.NotSupportedException">The exception that is thrown when an invoked method is not supported, or when there is an attempt to read, seek, or write to a stream that does not support the invoked functionality.</exception>
        protected internal virtual void StartCloning(FolderPair parameters)
        {
            DirectoryCloningInProgress = true;
            tspBtnRefresh.Enabled = !DirectoryCloningInProgress;
            tspBtnCancel.Enabled = DirectoryCloningInProgress;
            btnBrowseSource.Enabled = !DirectoryCloningInProgress;
            btnBrowseTarget.Enabled = !DirectoryCloningInProgress;

            if (_stopwatch != null)
            {
                _stopwatch.Reset();
                _stopwatch.Start();
            }

            _directoryCloner = new DirectoryCloner(parameters);
            _directoryCloner.FileCloneTaskStarted += FileCloneTaskStarted;
            _directoryCloner.FileCloneTaskCompleted += FileCloneTaskCompleted;
            _directoryCloner.FileCloneErrorOccurred += FileCloneErrorOccurred;
            _directoryCloner.BeginClone();

            SetSuccessStatus("Began directory clone, linking operations.");
        }

        /// <summary>
        /// Add the preamble statement block to logger window.
        /// </summary>
        /// <exception cref="System.ArgumentException">The exception that is thrown when one of the arguments provided to a method is not valid.</exception>
        /// <exception cref="System.IO.FileNotFoundException">The exception that is thrown when an attempt to access a file that does not exist on disk fails.</exception>
        /// <exception cref="System.IO.PathTooLongException">The exception that is thrown when a pathname or filename is longer than the system-defined maximum length.</exception>
        /// <exception cref="System.NotSupportedException">The exception that is thrown when an invoked method is not supported, or when there is an attempt to read, seek, or write to a stream that does not support the invoked functionality.</exception>
        protected internal virtual void WriteLoggerPreamble()
        {
            var exeName = Path.GetFileName(Assembly.GetExecutingAssembly().Location);
            var assemblyVer = Assembly.GetExecutingAssembly().GetName().Version.ToString();
            var prodVer = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion;
            var fileVer = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion;
            var copyright = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).LegalCopyright;

            LogMessageToWindow("Starting '" + exeName + "'");
            LogMessageToWindow(copyright);
            LogMessageToWindow("File location is: '" + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) +
                               "'");
            LogMessageToWindow("Assembly Version is: " + assemblyVer);
            LogMessageToWindow("Product Version is: " + prodVer);
            LogMessageToWindow("File Version is: " + fileVer);
            LogMessageToWindow("License is: GNU Public License V3.29");
        }

        #endregion Methods

        #region Nested Types

        /// <summary>
        /// Arguments needed to refresh the <see cref="ObjectListView"/>
        /// </summary>
        public class RefreshListViewArgs
        {
            #region Constructors

            /// <summary>
            /// Constructor.
            /// </summary>
            /// <param name="source">Source folder</param>
            /// <param name="destination">Destination folder</param>
            public RefreshListViewArgs(string source, string destination)
            {
                Source = source;
                Destination = destination;
            }

            #endregion Constructors

            #region Properties

            /// <summary>
            /// Destination folder
            /// </summary>
            public string Destination
            {
                get; set;
            }

            /// <summary>
            /// Source folder
            /// </summary>
            public string Source
            {
                get; set;
            }

            #endregion Properties
        }

        #endregion Nested Types
    }

    /// <summary>
    /// <see cref="EventArgs"/> for notifications to a refreshListView events 
    /// </summary>
    public class RefreshEventArgs : EventArgs
    {
        #region Constructors

        /// <summary>
        /// Constructor.
        /// </summary>
        public RefreshEventArgs() : this(new List<FolderPair>(), string.Empty) { }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="folderPairs">The <see cref="FolderPair"/> <see cref="List{T}"/>.</param>
        /// <param name="message">A <see cref="string"/> message related to the event.</param>
        public RefreshEventArgs(List<FolderPair> folderPairs, string message)
        {
            FolderPairs = folderPairs;
            Message = message;
        }

        #endregion Constructors

        #region Properties

        /// <summary>
        /// The <see cref="FolderPair"/> <see cref="List{T}"/>.
        /// </summary>
        public virtual List<FolderPair> FolderPairs { get; set; }

        /// <summary>
        /// A <see cref="string"/> message related to the event.
        /// </summary>
        public virtual string Message { get; set; }

        #endregion Properties
    }
}